Hola 😀

Soy Hesus Garcia como "Jesús" pero con H. Sé que puede ser confuso al principio, pero una vez que lo recuerdes, ¡nunca lo olvidarás! 🌝 . Como revisor de código de Practicum, estoy emocionado de examinar tus proyectos y ayudarte a mejorar tus habilidades en programación. si has cometido algún error, no te preocupes, pues ¡estoy aquí para ayudarte a corregirlo y hacer que tu código brille! 🌟. Si encuentro algún detalle en tu código, te lo señalaré para que lo corrijas, ya que mi objetivo es ayudarte a prepararte para un ambiente de trabajo real, donde el líder de tu equipo actuaría de la misma manera. Si no puedes solucionar el problema, te proporcionaré más información en la próxima oportunidad. Cuando encuentres un comentario, por favor, no los muevas, no los modifiques ni los borres.

Revisaré cuidadosamente todas las implementaciones que has realizado para cumplir con los requisitos y te proporcionaré mis comentarios de la siguiente manera:

Comentario del revisor Si todo está perfecto.
Comentario del revisor Si tu código está bien pero se puede mejorar o hay algún detalle que le hace falta.
Comentario del revisor Si de pronto hace falta algo o existe algún problema con tu código o conclusiones.

Puedes responderme de esta forma:

Respuesta del estudiante

¡Empecemos! 🚀

Telecomunicaciones: Identificar operadores ineficientes¶

El objetivo de este análisis es determinar cuando un operador es ineficiente en sus labores. Estaremos importando dos tablas: La primera con la información de las llamadas recibidas e información del operador que recibió la llamada.

Los campos que contiene esta primera tabla son los siguientes:

  • user_id: ID de la cuenta de cliente
  • date: fecha en la que se recuperaron las estadísticas
  • direction: "dirección" de llamada (out para saliente, in para entrante)
  • internal: si la llamada fue interna (entre los operadores de un cliente o clienta)
  • operator_id: identificador del operador
  • is_missed_call: si fue una llamada perdida
  • calls_count: número de llamadas
  • call_duration: duración de la llamada (sin incluir el tiempo de espera)
  • total_call_duration: duración de la llamada (incluido el tiempo de espera)

La segunda tabla que estaremos importando contiene la información de los clientes y se organiza de la siguiente manera:

  • user_id: ID de usuario/a
  • tariff_plan: tarifa actual de la clientela
  • date_start: fecha de registro de la clientela

Las métricas para evaluar si un operador es eficiente en sus funciones serán tres:

  • Una proporción de llamadas perdidas altas
  • Un tiempo de espera muy alto antes de atender una llamada
  • Pocas llamadas salientes para operadores que cuyas funciones incluyan llamar a clientes

Luego de determinar estas tres métricas, procederemos a análizar hipótesis con base en las conclusiones anteriores.

Tabla de contenido¶

  • 2 Importando datos
  • 3 Exploración inicial
  • 4 Preprocesamiento de datos
    • 4.1 Valores nulos
    • 4.2 Duplicados
    • 4.3 Enriqueciendo los datos
  • 5 Análisis exploratorio de datos
    • 5.1 Distribución de datos
    • 5.2 Datos por operador
    • 5.3 Definiendo umbrales
    • 5.4 Comparando operadores
  • 6 Prueba de hipótesis
    • 6.1 Cantidad de llamadas entre operadores receptores
    • 6.2 Duración de llamdas entre operadores receptores
    • 6.3 Duración de llamadas entre operadores emisores
  • 6 Prueba de hipótesis
Comentario del revisor Me parece genial que hayas incluido una tabla de contenidos en tu documento, esto facilitará la navegación y comprensión de los temas que estás tratando. ¡Sigue así!

Importando datos¶

Importaremos las librerías necesarias para nuestros análisis, para posteriormente, importar 2 tablas que contienen los datos necesarios.

La primera tabla, ubicada en la ruta /datasets/telecom_dataset_us.csv contiene los datos sobre el desempeño de los operadores que están siendo evaluados.

La segunda tabla, ubicada en la ruta /datasets/telecom_clients_us.csv contiene los datos de los clientes que realizan llamadas al centro de llamadas donde están ubicados los operadores del análisis.

Regresar

In [5]:
# importando librerías

import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objs as go

from scipy import stats as st
from statistics import mode
Comentario del revisor Quería proporcionarte algunos comentarios sobre la organización de los imports en tu código. Entiendo que esto se te proporcionó como parte de una plantilla, sin embargo es importante destacar el orden de los imports.

Es preferible agrupar los imports siguiendo el siguiente orden:

Imports de la biblioteca estándar de Python. Imports de bibliotecas de terceros relacionadas. Imports específicos de la aplicación local o biblioteca personalizada. Para mejorar la legibilidad del código, también es recomendable dejar una línea en blanco entre cada grupo de imports, pero solo un import por línea. Te dejo esta referencia con ejemplos:
https://pep8.org/#imports

In [6]:
# Creando variable para declarar la ruta de los archivos

path = "/datasets/"
# os.path.join()
# importando tablas

calls = pd.read_csv(path+"telecom_dataset_us.csv")
clients = pd.read_csv(path+"telecom_clients_us.csv")
Comentario del revisor Muy buena intuición para generar un path. Sin embargo, es mejor utilizar os.path.join() en lugar de codificar las rutas de los archivos, ya que hace que el código sea más independiente de la plataforma y resistente a los cambios en la estructura de directorios.

Al utilizar os.path.join(), podemos crear rutas de archivos que sean independientes de la plataforma, lo que significa que funcionarán tanto en sistemas Windows como en sistemas basados en Unix. Esto se debe a que os.path.join() automáticamente utiliza el separador de ruta adecuado (\ en Windows y / en sistemas basados en Unix) para unir los componentes de la ruta.

Conclusión

Una vez importados nuestros datos procederemos a realizar la exploración de los datos nuestras tablas.

Exploración inicial¶

Mostraremos como se distribuyen nuestros datos, tipos de datos, si contienen valores nulos y duplicados implícitos.

Regresar

Llamadas

In [7]:
# Mostrando información general de la tabla

calls.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 53902 entries, 0 to 53901
Data columns (total 9 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   user_id              53902 non-null  int64  
 1   date                 53902 non-null  object 
 2   direction            53902 non-null  object 
 3   internal             53785 non-null  object 
 4   operator_id          45730 non-null  float64
 5   is_missed_call       53902 non-null  bool   
 6   calls_count          53902 non-null  int64  
 7   call_duration        53902 non-null  int64  
 8   total_call_duration  53902 non-null  int64  
dtypes: bool(1), float64(1), int64(4), object(3)
memory usage: 11.1 MB
In [8]:
# Mostrando primeras 15 filas de la tabla

calls.head(15)
Out[8]:
user_id date direction internal operator_id is_missed_call calls_count call_duration total_call_duration
0 166377 2019-08-04 00:00:00+03:00 in False NaN True 2 0 4
1 166377 2019-08-05 00:00:00+03:00 out True 880022.0 True 3 0 5
2 166377 2019-08-05 00:00:00+03:00 out True 880020.0 True 1 0 1
3 166377 2019-08-05 00:00:00+03:00 out True 880020.0 False 1 10 18
4 166377 2019-08-05 00:00:00+03:00 out False 880022.0 True 3 0 25
5 166377 2019-08-05 00:00:00+03:00 out False 880020.0 False 2 3 29
6 166377 2019-08-05 00:00:00+03:00 out False 880020.0 True 8 0 50
7 166377 2019-08-05 00:00:00+03:00 in False NaN True 6 0 35
8 166377 2019-08-05 00:00:00+03:00 out False 880020.0 True 8 0 50
9 166377 2019-08-06 00:00:00+03:00 in False NaN True 4 0 62
10 166377 2019-08-06 00:00:00+03:00 out False 881278.0 True 3 0 29
11 166377 2019-08-06 00:00:00+03:00 out False 880020.0 True 5 0 70
12 166377 2019-08-06 00:00:00+03:00 out True 881278.0 True 2 0 5
13 166377 2019-08-06 00:00:00+03:00 out False 880020.0 False 5 800 819
14 166377 2019-08-07 00:00:00+03:00 out False 880028.0 True 1 0 15
In [9]:
# Mostrando últimas 15 filas de la tabla

calls.tail(15)
Out[9]:
user_id date direction internal operator_id is_missed_call calls_count call_duration total_call_duration
53887 168603 2019-11-21 00:00:00+03:00 out False 959118.0 False 5 338 423
53888 168603 2019-11-27 00:00:00+03:00 out False 959118.0 False 1 76 99
53889 168603 2019-11-28 00:00:00+03:00 in False NaN True 1 0 30
53890 168606 2019-11-08 00:00:00+03:00 out False 957922.0 True 2 0 40
53891 168606 2019-11-08 00:00:00+03:00 in False 957922.0 True 1 0 7
53892 168606 2019-11-08 00:00:00+03:00 out False 957922.0 False 2 255 328
53893 168606 2019-11-08 00:00:00+03:00 in False NaN True 6 0 121
53894 168606 2019-11-08 00:00:00+03:00 in False 957922.0 False 2 686 705
53895 168606 2019-11-09 00:00:00+03:00 out False 957922.0 False 4 551 593
53896 168606 2019-11-10 00:00:00+03:00 out True 957922.0 False 1 0 25
53897 168606 2019-11-10 00:00:00+03:00 out True 957922.0 True 1 0 38
53898 168606 2019-11-11 00:00:00+03:00 out True 957922.0 False 2 479 501
53899 168606 2019-11-15 00:00:00+03:00 out True 957922.0 False 4 3130 3190
53900 168606 2019-11-15 00:00:00+03:00 out True 957922.0 False 4 3130 3190
53901 168606 2019-11-19 00:00:00+03:00 in False NaN True 2 0 64
In [10]:
# Mostrando distribución general de los datos.

calls.describe()
Out[10]:
user_id operator_id calls_count call_duration total_call_duration
count 53902.000000 45730.000000 53902.000000 53902.000000 53902.000000
mean 167295.344477 916535.993002 16.451245 866.684427 1157.133297
std 598.883775 21254.123136 62.917170 3731.791202 4403.468763
min 166377.000000 879896.000000 1.000000 0.000000 0.000000
25% 166782.000000 900788.000000 1.000000 0.000000 47.000000
50% 167162.000000 913938.000000 4.000000 38.000000 210.000000
75% 167819.000000 937708.000000 12.000000 572.000000 902.000000
max 168606.000000 973286.000000 4817.000000 144395.000000 166155.000000
In [11]:
# Buscando valores nulos

calls.isna().sum()
Out[11]:
user_id                   0
date                      0
direction                 0
internal                117
operator_id            8172
is_missed_call            0
calls_count               0
call_duration             0
total_call_duration       0
dtype: int64
In [12]:
# Buscando duplicados

calls.duplicated().sum()
Out[12]:
4900
In [13]:
# Calculando porcentaje de duplicados en los datos

calls.duplicated().sum() / len(calls)
Out[13]:
0.09090571778412675
Comentarios del Revisor
Correcto, info(), head() y describe() son herramientas esceneciales que nos ayudaran a hacer un análisis exploratorio inicial. Continúa con el buen trabajo!

Conclusión intermedia

La tabla de llamadas cuenta con 53902 registros, de los cuales tenemos 4900 duplicados que representan un 9% de los datos.

Adicionalmente, tenemos 2 columnas con valores nulos, la columna que nos indica si una llamada fue o no interna, y la columna que identifica al operador que recibe o hace la llamada.

También debemos considerar que hay que corregir los datos de la columna de "fecha", y la columna de "internal", esta última luego de verificar la naturaleza de los valores nulos.

Llamadas

In [14]:
# Mostrando información general de la tabla

clients.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 732 entries, 0 to 731
Data columns (total 3 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   user_id      732 non-null    int64 
 1   tariff_plan  732 non-null    object
 2   date_start   732 non-null    object
dtypes: int64(1), object(2)
memory usage: 95.2 KB
In [15]:
# Mostrando primeras 15 filas de la tabla

clients.head(15)
Out[15]:
user_id tariff_plan date_start
0 166713 A 2019-08-15
1 166901 A 2019-08-23
2 168527 A 2019-10-29
3 167097 A 2019-09-01
4 168193 A 2019-10-16
5 167764 A 2019-09-30
6 167050 A 2019-08-29
7 168252 A 2019-10-17
8 168495 A 2019-10-28
9 167879 A 2019-10-03
10 168573 A 2019-10-31
11 166610 A 2019-08-12
12 167536 A 2019-09-19
13 168354 A 2019-10-23
14 168284 A 2019-10-20
In [16]:
# Mostrando últimas 15 filas de la tabla

clients.tail(15)
Out[16]:
user_id tariff_plan date_start
717 167415 B 2019-09-16
718 166941 B 2019-08-26
719 166705 B 2019-08-15
720 166587 B 2019-08-09
721 167452 B 2019-09-17
722 166797 B 2019-08-20
723 167268 B 2019-09-10
724 166522 B 2019-08-07
725 166815 B 2019-08-20
726 166702 B 2019-08-15
727 166554 B 2019-08-08
728 166911 B 2019-08-23
729 167012 B 2019-08-28
730 166867 B 2019-08-22
731 166565 B 2019-08-08
In [17]:
# Mostrando distribución general de los datos.

clients.describe(include='all')
Out[17]:
user_id tariff_plan date_start
count 732.000000 732 732
unique NaN 3 73
top NaN C 2019-09-24
freq NaN 395 24
mean 167431.927596 NaN NaN
std 633.810383 NaN NaN
min 166373.000000 NaN NaN
25% 166900.750000 NaN NaN
50% 167432.000000 NaN NaN
75% 167973.000000 NaN NaN
max 168606.000000 NaN NaN
In [18]:
# Buscando valores nulos

clients.isna().sum()
Out[18]:
user_id        0
tariff_plan    0
date_start     0
dtype: int64
In [19]:
# Buscando duplicados

clients.duplicated().sum()
Out[19]:
0
In [20]:
# Calculando porcentaje de duplicados en los datos

clients.duplicated().sum() / len(clients)
Out[20]:
0.0
Comentarios del Revisor
Muy bien por verificar duplicados de clientes.

Conclusión intermedia

La tabla de clientes se importó correctamente, solo tendríamos que cambiar el tipo de datos de la columna date_start a "datetime".

Comentarios del Revisor
Correcto , hay que procesar tipos.

Preprocesamiento de datos¶

Abordaremos los valores duplicados, nulos y tipos de datos en las columnas hallados en la exploración inicial.

Regresar

Valores nulos¶

Iterearemos sobre cada variable categórica en la tabla para buscar un patrón sobre los valores ausentes.

Regresar

Internal

In [21]:
# Creando lista de nombre de columnas para iterar sobre ellas

categorical_c = ['direction', 'internal', 'is_missed_call']

# Iterando sobre las columnas categóricas para determinar qué variable está relacionada con estos valores nulos

for column in categorical_c:
    print(f"""
Comparando en columna \033[1m{column}\033[0m
{calls[column].value_counts(normalize=True)}

 \033[1m{column} sin valores ausentes\033[0m
{calls.dropna(subset=['internal'])[column].value_counts(normalize=True)}
    """)
Comparando en columna direction
out    0.59213
in     0.40787
Name: direction, dtype: float64

 direction sin valores ausentes
out    0.593381
in     0.406619
Name: direction, dtype: float64
    

Comparando en columna internal
False    0.885396
True     0.114604
Name: internal, dtype: float64

 internal sin valores ausentes
False    0.885396
True     0.114604
Name: internal, dtype: float64
    

Comparando en columna is_missed_call
False    0.562762
True     0.437238
Name: is_missed_call, dtype: float64

 is_missed_call sin valores ausentes
False    0.562889
True     0.437111
Name: is_missed_call, dtype: float64
    
In [22]:
# Calculando proporción de valores nulos en la columna internal

calls['internal'].isna().sum() / len(calls)
Out[22]:
0.0021706059144373123
Comentarios del Revisor
Excelente uso de los f strings

Conclusión intermedia

Los valores nulos de la variable se distribuye de forma muy parecida entre las variables categóricas, considerando que estamos tratado con el 0.2% de los registros en la tabla, eliminaremos los mismos.

In [23]:
# Eliminando filas con valores nulos en la columna internal

calls = (calls
         .dropna(subset=['internal'])
         .reset_index(drop=True)
        )

# Comprobando cantidad de valores nulos

calls.isna().sum()
Out[23]:
user_id                   0
date                      0
direction                 0
internal                  0
operator_id            8115
is_missed_call            0
calls_count               0
call_duration             0
total_call_duration       0
dtype: int64
Comentarios del Revisor
Muy buen uso del encadeamiento de métodos

Conclusión intermedia

Eliminamos las filas con valores nulos en la columna internal debido a que es una variable clave para responder a las preguntas de nuestro análisis.

Operator_id

In [24]:
# Iterando sobre las columnas categóricas para determinar qué variable está relacionada con estos valores nulos

for column in categorical_c:
    print(f"""
Comparando en columna \033[1m{column}\033[0m
{calls[column].value_counts(normalize=True)}

 \033[1m{column} sin valores ausentes\033[0m
{calls.dropna(subset=['operator_id'])[column].value_counts(normalize=True)}
    """)
Comparando en columna direction
out    0.593381
in     0.406619
Name: direction, dtype: float64

 direction sin valores ausentes
out    0.694482
in     0.305518
Name: direction, dtype: float64
    

Comparando en columna internal
False    0.885396
True     0.114604
Name: internal, dtype: float64

 internal sin valores ausentes
False    0.872805
True     0.127195
Name: internal, dtype: float64
    

Comparando en columna is_missed_call
False    0.562889
True     0.437111
Name: is_missed_call, dtype: float64

 is_missed_call sin valores ausentes
False    0.660236
True     0.339764
Name: is_missed_call, dtype: float64
    
In [25]:
# Calculando proporción de valores nulos en la columna internal

calls['operator_id'].isna().sum() / len(calls)
Out[25]:
0.15087849772241332

Conclusión intermedia

Dada la naturaleza de la variable y la importancia que tiene para nuestro análisis, completaremos los valores ausentes con el string unknown e informaremos al equipo encargado de recaudar los datos para evitar que ocurra más adelante.

Estaremos completando el 15.08% de los valores ausentes como unknown.

In [26]:
# Completando valores ausentes

calls['operator_id'].fillna('unknown', inplace=True)

# Calculando cantidad de valores nulos

calls.isna().sum()
Out[26]:
user_id                0
date                   0
direction              0
internal               0
operator_id            0
is_missed_call         0
calls_count            0
call_duration          0
total_call_duration    0
dtype: int64

Duplicados¶

Eliminaremos los duplicados implícitos

Regresar

In [27]:
# Eliminando duplicados implícitos

calls = calls.drop_duplicates().reset_index(drop=True)

# Comprobando la presencia de duplicados

calls.duplicated().sum()
Out[27]:
0
In [28]:
# Comprobando nuevo tamaño de la tabla de llamadas

calls.shape[0]
Out[28]:
48892

Conclusión intermedia

Eliminamos los duplicados implícitos, estos a su vez representaban el 9% de los datos. Nos quedamos con 48892 registros de llamadas para el análisis.

Ahora procederemos al enriquecimiento de los datos.

Enriqueciendo los datos¶

Realizaremos las modificaciones mencionadas en la fase de exploración de los datos para adecuar los datos para el análisis.

Regresar

Llamadas

In [29]:
# Cambiando tipo de datos en la columna de fecha

calls['date'] = (
    # Convirtiendo de object a datetime
    pd.to_datetime(calls['date'],
                   format="%Y-%m-%d %H:%M:%S%z",
                   utc=False)
    
    # Extrayendo solo fecha y hora sin el Huso horario
    .dt.strftime("%Y-%m-%d %H:%M:%S")
    
    # Extrayendo el día de la llamada
).astype("datetime64[D]")

calls['date']
Out[29]:
0       2019-08-04
1       2019-08-05
2       2019-08-05
3       2019-08-05
4       2019-08-05
           ...    
48887   2019-11-10
48888   2019-11-10
48889   2019-11-11
48890   2019-11-15
48891   2019-11-19
Name: date, Length: 48892, dtype: datetime64[ns]
In [30]:
# Incluyendo semana de la llamada en la tabla

calls['call_week'] =  calls['date'].dt.isocalendar().week

calls[['date','call_week']]
Out[30]:
date call_week
0 2019-08-04 31
1 2019-08-05 32
2 2019-08-05 32
3 2019-08-05 32
4 2019-08-05 32
... ... ...
48887 2019-11-10 45
48888 2019-11-10 45
48889 2019-11-11 46
48890 2019-11-15 46
48891 2019-11-19 47

48892 rows × 2 columns

Conclusión intermedia

Convertimos los datos al tipo de dato datetime64, e incluimos la semana en la que fue realizada la llamada.

In [31]:
# Calculando el tiempo que tardó la llamada en ser tomada

calls['ring_time'] = calls['total_call_duration'] - calls['call_duration']

calls[['is_missed_call','total_call_duration','call_duration','ring_time']].head(10)
Out[31]:
is_missed_call total_call_duration call_duration ring_time
0 True 4 0 4
1 True 5 0 5
2 True 1 0 1
3 False 18 10 8
4 True 25 0 25
5 False 29 3 26
6 True 50 0 50
7 True 35 0 35
8 True 62 0 62
9 True 29 0 29

Conclusión intermedia

Tenemos la cantidad de segundos que pasó el teléfono en espera antes de ser tomado o en su defecto, cerrado la llamada.

Clientes

In [32]:
# Convirtiendo la fecha de la tabla de clientes a datetime

clients['date_start'] = clients['date_start'].astype("datetime64[D]")

clients['date_start']
Out[32]:
0     2019-08-15
1     2019-08-23
2     2019-10-29
3     2019-09-01
4     2019-10-16
         ...    
727   2019-08-08
728   2019-08-23
729   2019-08-28
730   2019-08-22
731   2019-08-08
Name: date_start, Length: 732, dtype: datetime64[ns]

Conclusión

Convertimos la columna de fechas de la tabla de clientes a datetime.

Uniendo tablas

Incluiremos la información de los clientes en la tabla de llamadas para identificar fecha de inicio del ciclo de vida y el plan que tiene un cliente determinado.

In [33]:
# Uniendo tablas

calls = calls.merge(clients, on='user_id', how='left')

calls
Out[33]:
user_id date direction internal operator_id is_missed_call calls_count call_duration total_call_duration call_week ring_time tariff_plan date_start
0 166377 2019-08-04 in False unknown True 2 0 4 31 4 B 2019-08-01
1 166377 2019-08-05 out True 880022.0 True 3 0 5 32 5 B 2019-08-01
2 166377 2019-08-05 out True 880020.0 True 1 0 1 32 1 B 2019-08-01
3 166377 2019-08-05 out True 880020.0 False 1 10 18 32 8 B 2019-08-01
4 166377 2019-08-05 out False 880022.0 True 3 0 25 32 25 B 2019-08-01
... ... ... ... ... ... ... ... ... ... ... ... ... ...
48887 168606 2019-11-10 out True 957922.0 False 1 0 25 45 25 C 2019-10-31
48888 168606 2019-11-10 out True 957922.0 True 1 0 38 45 38 C 2019-10-31
48889 168606 2019-11-11 out True 957922.0 False 2 479 501 46 22 C 2019-10-31
48890 168606 2019-11-15 out True 957922.0 False 4 3130 3190 46 60 C 2019-10-31
48891 168606 2019-11-19 in False unknown True 2 0 64 47 64 C 2019-10-31

48892 rows × 13 columns

Comentario del revisor ¡Muy bien! 👏👏 Los cálculos de esta sección están correctos y eso es un gran logro. Sigue así y verás cómo poco a poco te irás convirtiendo en un experto en esta área. 💪💻

Conclusión

Ya con todos los datos incluidos, procederemos a iniciar el análisis exploratorio.

Análisis exploratorio de datos¶

Iniciaremos trazando histogramas sobre nuestras variables para observar como se distribuyen nuestros datos.

Regresar

Distribución de datos¶

Trazaremos histogramas para observar como se distribuyen los datos.

Regresar

In [34]:
# Creando lista de columnas para iterar

distribution_list = ['call_week', 'direction', 'internal', 'is_missed_call', 'calls_count', 'call_duration',
                    'total_call_duration', 'ring_time', 'tariff_plan']

time_distribution_list = ['calls_count', 'call_duration', 'total_call_duration', 'ring_time']

# Trazando histogramas

for i in distribution_list:
    
    if i in time_distribution_list:
        
        hist = px.histogram(calls, x=i, title=f'Distribution of {i}', log_y=True)
        hist.show()
    
    else:
        
        hist = px.histogram(calls, x=i, title=f'Distribution of {i}')
        hist.show()

Conclusión intermedia

Una vez observadas las distribuciones, nos percatamos que tenemos valores atípicos en las variables calls_count, call_duration, total_call_duration y ring_time. Todas estas estrechamente vinculadas.

Adicionalmente, vemos que para las semanas 31 a 34 existen pocos registros para analizar. Filtraremos la tabla excluyendo estos valores atípicos, filtraremos para los valores que estén por debajo del cuantil .95 para luego trazar nuevamente los histogramas y ver como cambiaron los datos.

In [35]:
# Calculando número total de filas antes de filtrar la tabla

calls.shape[0]
Out[35]:
48892
In [36]:
filtered_calls = (
    calls[(calls['call_week'] >34)&
          (calls['calls_count'] <= calls['calls_count'].quantile(.95)) &
          (calls['call_duration'] <= calls['call_duration'].quantile(.95)) &
          (calls['total_call_duration'] <= calls['total_call_duration'].quantile(.95)) &
          (calls['ring_time'] <= calls['ring_time'].quantile(.95))
         ]
).reset_index(drop=True)

filtered_calls
Out[36]:
user_id date direction internal operator_id is_missed_call calls_count call_duration total_call_duration call_week ring_time tariff_plan date_start
0 166377 2019-08-26 out True 880022.0 True 3 0 0 35 0 B 2019-08-01
1 166377 2019-08-26 in False 880028.0 False 2 285 302 35 17 B 2019-08-01
2 166377 2019-08-26 out False 880026.0 False 28 3298 3395 35 97 B 2019-08-01
3 166377 2019-08-26 out False 880028.0 True 4 0 241 35 241 B 2019-08-01
4 166377 2019-08-26 out False 880022.0 False 3 1079 1093 35 14 B 2019-08-01
... ... ... ... ... ... ... ... ... ... ... ... ... ...
43786 168606 2019-11-10 out True 957922.0 False 1 0 25 45 25 C 2019-10-31
43787 168606 2019-11-10 out True 957922.0 True 1 0 38 45 38 C 2019-10-31
43788 168606 2019-11-11 out True 957922.0 False 2 479 501 46 22 C 2019-10-31
43789 168606 2019-11-15 out True 957922.0 False 4 3130 3190 46 60 C 2019-10-31
43790 168606 2019-11-19 in False unknown True 2 0 64 47 64 C 2019-10-31

43791 rows × 13 columns

In [37]:
# Calculando proporción de datos de datos eliminados

(calls.shape[0] - filtered_calls.shape[0]) / calls.shape[0]
Out[37]:
0.10433199705473288

Conclusión intermedia

Eliminamos el 10.43% de los datos para deshacernos de los valores atípicos, trazaremos nuevamente las distribuciones para ver como han cambiado nuestros datos.

In [38]:
# Trazando histogramas

for i in distribution_list:
    
    if i in time_distribution_list:
        
        hist = px.histogram(filtered_calls, x=i, title=f'Distribution of {i}', log_y=True)
        hist.show()
    
    else:
        
        hist = px.histogram(filtered_calls, x=i, title=f'Distribution of {i}')
        hist.show()

Conclusión

Luego de filtrar los datos observamos que se mantienen una cantidad importante de valores atípicos en las variables calls_duration, total_calls_duration y ring_time. Sin embargo, procederemos con el análisis, considerndo que debido al alto volumen de llamadas perdidas, los datos están sesgados a la derecha.

Comentario del revisor ¡Muy bien! 👏👏 Has hecho buen uso de las buenas prtácticas de visualización de datos y optimizado tus cálculos para esta sección 💻

Datos por operador¶

Agruparemos las variables existentes en nuestra tabla de llamadas por operador para calcular los datos generales sobre su rendimiento.

Regresar

In [39]:
# Calculando agregación por operador y cantidad de llamadas atendidas y no atendidas

calls_by_operators = (filtered_calls
                      .groupby(['call_week','operator_id', 'is_missed_call'], as_index=False)
                      .agg({'calls_count':'sum'})
                     )

# Transponiendo los resultados

calls_by_operators = (calls_by_operators
                      .pivot(index=['call_week', 'operator_id'], columns='is_missed_call', values='calls_count')
                      .reset_index()
                      .fillna(0)
                     )

# Cambiando nombre de las columnas

calls_by_operators.columns = ['call_week','operator_id', 'calls_count', 'missed_calls_count']

# Cambiando tipo de dato a entero de las columnas de cantida de llamadas

calls_by_operators[['calls_count', 'missed_calls_count']] = (calls_by_operators[['calls_count', 'missed_calls_count']]
                                                             .astype('int')
                                                            )
# Calculando proporción de llamadas concretadas (salientes y entrantes)

calls_by_operators['missed_calls_proportion'] = abs(calls_by_operators['calls_count'] / (calls_by_operators['calls_count'] +
                                                                                     calls_by_operators['missed_calls_count'])
                                                    -1)

calls_by_operators
Out[39]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion
0 35 879896.0 32 94 0.746032
1 35 879898.0 8 33 0.804878
2 35 880022.0 4 3 0.428571
3 35 880026.0 113 52 0.315152
4 35 880028.0 86 84 0.494118
... ... ... ... ... ...
5547 48 972412.0 36 25 0.409836
5548 48 972460.0 23 28 0.549020
5549 48 973120.0 1 2 0.666667
5550 48 973286.0 2 0 0.000000
5551 48 unknown 6 2996 0.998001

5552 rows × 5 columns

Comentario del revisor Los cálculos son excelentes, recuerda que puedes mantener más cortas las líneas para que el código sea más legible, una longitud menor a 89 carácteres puede ser óptima

Conclusión intermedia

Calculamos el conteo de llamadas por operador (atendidas y perdidas), procederemos a calcular los tiempos promedios de las llamadas que sí fueron atendidas.

In [40]:
# Agrupando por operador para calcular agregaciones, considerando que el operador tomó la llamada.

calls_duration = (filtered_calls
                  .loc[filtered_calls['is_missed_call'] == False]
                  .groupby(['call_week','operator_id'], as_index=False)
                  .agg({'call_duration':['mean', 'median']})
                  .reset_index(drop=True)
                 )

# Renombrando las columnas

calls_duration.columns = ['call_week','operator_id','avg_call_duration','median_call_duration']

calls_duration
Out[40]:
call_week operator_id avg_call_duration median_call_duration
0 35 879896.0 324.000000 251.0
1 35 879898.0 168.500000 168.5
2 35 880022.0 659.000000 659.0
3 35 880026.0 2211.333333 2512.5
4 35 880028.0 1191.000000 989.0
... ... ... ... ...
5220 48 972412.0 1166.000000 1450.0
5221 48 972460.0 304.500000 61.5
5222 48 973120.0 5.000000 5.0
5223 48 973286.0 17.000000 17.0
5224 48 unknown 172.000000 159.0

5225 rows × 4 columns

Comentario del revisor ¡Muy bien! Es importante que siempre hagamos un reset index para mantener consistencia de datos.

Conclusión intermedia

Realizadas las agregaciones de tiempo de duración de las llamadas, las incluiremos en la tabla principal de agregaciones por operador.

In [41]:
# Uniendo tablas de agregaciones a la tabla principal

calls_by_operators = calls_by_operators.merge(calls_duration, on=['call_week','operator_id'], how='left')

calls_by_operators
Out[41]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration
0 35 879896.0 32 94 0.746032 324.000000 251.0
1 35 879898.0 8 33 0.804878 168.500000 168.5
2 35 880022.0 4 3 0.428571 659.000000 659.0
3 35 880026.0 113 52 0.315152 2211.333333 2512.5
4 35 880028.0 86 84 0.494118 1191.000000 989.0
... ... ... ... ... ... ... ...
5547 48 972412.0 36 25 0.409836 1166.000000 1450.0
5548 48 972460.0 23 28 0.549020 304.500000 61.5
5549 48 973120.0 1 2 0.666667 5.000000 5.0
5550 48 973286.0 2 0 0.000000 17.000000 17.0
5551 48 unknown 6 2996 0.998001 172.000000 159.0

5552 rows × 7 columns

Conclusión intermedia

Luego de la unión de estas primeras tablas, calcularemos la cantidad de llamadas internas y externas por operador.

In [42]:
# Calculando agregación por operador y cantidad de llamadas internas

internal_calls = (filtered_calls
                  .loc[filtered_calls['is_missed_call'] == False]
                  .groupby(['call_week','operator_id', 'internal'], as_index=False)
                  .agg({'calls_count':'sum'})
                 )

# Transponiendo los resultados

internal_calls = (internal_calls
                  .pivot(index=['call_week','operator_id'], columns='internal', values='calls_count')
                  .reset_index()
                  .fillna(0)
                 )

# Cambiando nombre de las columnas

internal_calls.columns = ['call_week','operator_id', 'external_calls', 'internal_calls']

# Cambiando tipo de dato a entero de las columnas de cantida de llamadas

internal_calls[['external_calls', 'internal_calls']] = (internal_calls[['external_calls', 'internal_calls']]
                                                        .astype('int')
                                                       )
# Calculando proporción de llamadas externas

internal_calls['external_proportion'] = internal_calls['external_calls'] / (internal_calls['external_calls'] +
                                                                           internal_calls['internal_calls'])
internal_calls
Out[42]:
call_week operator_id external_calls internal_calls external_proportion
0 35 879896.0 32 0 1.000000
1 35 879898.0 8 0 1.000000
2 35 880022.0 4 0 1.000000
3 35 880026.0 113 0 1.000000
4 35 880028.0 86 0 1.000000
... ... ... ... ... ...
5220 48 972412.0 36 0 1.000000
5221 48 972460.0 22 1 0.956522
5222 48 973120.0 1 0 1.000000
5223 48 973286.0 2 0 1.000000
5224 48 unknown 2 4 0.333333

5225 rows × 5 columns

In [43]:
# Uniendo tabla de llamadas internas y externas a tabla con el resto de las agregaciones por operador

calls_by_operators = calls_by_operators.merge(internal_calls, on=['call_week','operator_id'], how='left')

calls_by_operators
Out[43]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration external_calls internal_calls external_proportion
0 35 879896.0 32 94 0.746032 324.000000 251.0 32.0 0.0 1.000000
1 35 879898.0 8 33 0.804878 168.500000 168.5 8.0 0.0 1.000000
2 35 880022.0 4 3 0.428571 659.000000 659.0 4.0 0.0 1.000000
3 35 880026.0 113 52 0.315152 2211.333333 2512.5 113.0 0.0 1.000000
4 35 880028.0 86 84 0.494118 1191.000000 989.0 86.0 0.0 1.000000
... ... ... ... ... ... ... ... ... ... ...
5547 48 972412.0 36 25 0.409836 1166.000000 1450.0 36.0 0.0 1.000000
5548 48 972460.0 23 28 0.549020 304.500000 61.5 22.0 1.0 0.956522
5549 48 973120.0 1 2 0.666667 5.000000 5.0 1.0 0.0 1.000000
5550 48 973286.0 2 0 0.000000 17.000000 17.0 2.0 0.0 1.000000
5551 48 unknown 6 2996 0.998001 172.000000 159.0 2.0 4.0 0.333333

5552 rows × 10 columns

Conclusión intermedia

Agregamos la cantidad de llamadas atendidas internas y externas. A continuación, calcularemos el tiempo que duró en atender la llamada un operador determinado.

In [44]:
# Calculando agregación por operadores, donde encontremos el promedio de "ring_time" para llamadas entrantes

avg_ring_time = (filtered_calls
                 .loc[(filtered_calls['is_missed_call'] == False) &
                     (filtered_calls['direction'] == 'in')]
                 .groupby(['call_week','operator_id'], as_index=False)
                 .agg({'ring_time':'mean'})
                )

avg_ring_time.columns = ['call_week','operator_id', 'avg_ring_time']

avg_ring_time
Out[44]:
call_week operator_id avg_ring_time
0 35 879896.0 70.25
1 35 879898.0 25.00
2 35 880026.0 4.00
3 35 880028.0 18.50
4 35 882680.0 41.80
... ... ... ...
3674 48 971354.0 42.00
3675 48 972412.0 25.00
3676 48 972460.0 4.00
3677 48 973286.0 88.00
3678 48 unknown 16.00

3679 rows × 3 columns

In [45]:
# Uniendo con tabla de datos por operador

calls_by_operators = calls_by_operators.merge(avg_ring_time, on=['call_week','operator_id'], how='left')

calls_by_operators
Out[45]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration external_calls internal_calls external_proportion avg_ring_time
0 35 879896.0 32 94 0.746032 324.000000 251.0 32.0 0.0 1.000000 70.25
1 35 879898.0 8 33 0.804878 168.500000 168.5 8.0 0.0 1.000000 25.00
2 35 880022.0 4 3 0.428571 659.000000 659.0 4.0 0.0 1.000000 NaN
3 35 880026.0 113 52 0.315152 2211.333333 2512.5 113.0 0.0 1.000000 4.00
4 35 880028.0 86 84 0.494118 1191.000000 989.0 86.0 0.0 1.000000 18.50
... ... ... ... ... ... ... ... ... ... ... ...
5547 48 972412.0 36 25 0.409836 1166.000000 1450.0 36.0 0.0 1.000000 25.00
5548 48 972460.0 23 28 0.549020 304.500000 61.5 22.0 1.0 0.956522 4.00
5549 48 973120.0 1 2 0.666667 5.000000 5.0 1.0 0.0 1.000000 NaN
5550 48 973286.0 2 0 0.000000 17.000000 17.0 2.0 0.0 1.000000 88.00
5551 48 unknown 6 2996 0.998001 172.000000 159.0 2.0 4.0 0.333333 16.00

5552 rows × 11 columns

Conclusión intermedia

Completamos las agregaciones por operador, por último, incluiremos una columna donde nos indique si el operador trabaja con llamadas salientes o recibe llamadas.

Para agregar esta columna, utilizaremos la tabla avg_ring_time donde los operadores que se reflejan en la misma, son los que trabajan recibiendo llamadas, el resto trabaja con llamadas salientes.

In [46]:
# Agregando columna con tipo de operación que realiza el operador.

calls_by_operators.loc[calls_by_operators['operator_id']
                       .isin(avg_ring_time['operator_id']), 'reciever_operator'] = True

# Completando los NaNs de la columna recipient_operator con False
calls_by_operators['reciever_operator'].fillna(False, inplace=True)

calls_by_operators
Out[46]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration external_calls internal_calls external_proportion avg_ring_time reciever_operator
0 35 879896.0 32 94 0.746032 324.000000 251.0 32.0 0.0 1.000000 70.25 True
1 35 879898.0 8 33 0.804878 168.500000 168.5 8.0 0.0 1.000000 25.00 True
2 35 880022.0 4 3 0.428571 659.000000 659.0 4.0 0.0 1.000000 NaN True
3 35 880026.0 113 52 0.315152 2211.333333 2512.5 113.0 0.0 1.000000 4.00 True
4 35 880028.0 86 84 0.494118 1191.000000 989.0 86.0 0.0 1.000000 18.50 True
... ... ... ... ... ... ... ... ... ... ... ... ...
5547 48 972412.0 36 25 0.409836 1166.000000 1450.0 36.0 0.0 1.000000 25.00 True
5548 48 972460.0 23 28 0.549020 304.500000 61.5 22.0 1.0 0.956522 4.00 True
5549 48 973120.0 1 2 0.666667 5.000000 5.0 1.0 0.0 1.000000 NaN False
5550 48 973286.0 2 0 0.000000 17.000000 17.0 2.0 0.0 1.000000 88.00 True
5551 48 unknown 6 2996 0.998001 172.000000 159.0 2.0 4.0 0.333333 16.00 True

5552 rows × 12 columns

Conclusión

Finalizada la tabla de agregaciones por operador, observaremos las distribuciones de los campos contenidos en esta tabla para determinar umbrales donde identificaremos a los operadores no eficientes.

Definiendo umbrales ¿Cómo identifico si un operador no está siendo eficiente?¶

Observaremos las distribuciones y nos basaremos en los límites teóricos superiores o inferiores para determinar si un operador está siendo eficiente en sus labores, dependiendo del tipo de métrica que estemos evaluando.

Regresar

Llamadas perdidas

Observaremos como se distribuyen las proporciones de llamadas perdidas por operador, considerando que sus funciones incluyan recibir llamadas.

In [47]:
# Creando tabla de agregaciones sin los operadores "desconocidos"

reciever_operators = (calls_by_operators
                      .loc[(calls_by_operators['operator_id'] != "unknown") &
                          (calls_by_operators['reciever_operator'] == True)]
                      .reset_index(drop=True)
                     )

reciever_operators
Out[47]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration external_calls internal_calls external_proportion avg_ring_time reciever_operator
0 35 879896.0 32 94 0.746032 324.000000 251.0 32.0 0.0 1.000000 70.25 True
1 35 879898.0 8 33 0.804878 168.500000 168.5 8.0 0.0 1.000000 25.00 True
2 35 880022.0 4 3 0.428571 659.000000 659.0 4.0 0.0 1.000000 NaN True
3 35 880026.0 113 52 0.315152 2211.333333 2512.5 113.0 0.0 1.000000 4.00 True
4 35 880028.0 86 84 0.494118 1191.000000 989.0 86.0 0.0 1.000000 18.50 True
... ... ... ... ... ... ... ... ... ... ... ... ...
4330 48 971102.0 50 0 0.000000 2004.333333 2980.0 50.0 0.0 1.000000 286.00 True
4331 48 971354.0 6 0 0.000000 371.500000 371.5 6.0 0.0 1.000000 42.00 True
4332 48 972412.0 36 25 0.409836 1166.000000 1450.0 36.0 0.0 1.000000 25.00 True
4333 48 972460.0 23 28 0.549020 304.500000 61.5 22.0 1.0 0.956522 4.00 True
4334 48 973286.0 2 0 0.000000 17.000000 17.0 2.0 0.0 1.000000 88.00 True

4335 rows × 12 columns

In [48]:
# Trazando histograma para variable missed_calls_count

px.box(reciever_operators,x='call_week', y='missed_calls_proportion', title='Missed calls proportion by operators throughout the weeks')

Conclusión intermedia

En todas las semanas tenemos las distribuciones sesgadas a la derecha. A partir de la cuarta semana, encontramos que las distribuciones se concentran aún más entre las proporciones bajas de llamadas perdidas, y son pocos los operadores que se encuentran en ese sesgo, de hecho, podemos observar varios valores atípicos que nos demuestran que los operadores cada vez tienen un menor porcentaje de llamadas perdidas.

Calcularemos un promedio entre la mediana y la media de esta distribución para determinar un umbral que identifique si el operador en cuestión está en desempeño deficiente.

Comentario del revisor ¡Muy bien! 👏👏 por el desarrollo de tu propia mética para identificar los operadores ineficientes, algunos otros alumnos incluyen el valor z, en tu caso me parece interesante que consideres el sesgo acentuandolo con la media
In [49]:
# Calculando umbral para determinar si un operador no fue eficiente recibiendo llamadas.

missed_calls_threshold = ((reciever_operators['missed_calls_proportion'].median() +
                         reciever_operators['missed_calls_proportion'].mean()) / 2).round(2)

missed_calls_threshold
Out[49]:
0.19

Conclusión

De acuerdo a los cálculos, un operador no debe promediar más de un 19% de llamadas perdidas por semana para no considerarse como "no eficiente".

Continuaremos calculando el resto de los umbrales antes de determinar la efectividad de los operadores.

Tiempo de espera

Observaremos como se distribuyen las proporciones de los tiempos de espera por operador, considerando que sus funciones incluyan recibir llamadas.

In [50]:
# Trazando histograma para variable ring_time

px.box(reciever_operators,x='call_week' ,y='avg_ring_time', title='Average ring time on incoming calls throughout the weeks')

Conclusión intermedia

Observando las distribuciones por semana, se evidencia una gran presencia de valores atípicos para todas las semanas, por lo tanto, para este umbral, utilizaremos el promedio general de la variable avg_ring_time.

In [51]:
# Calculando umbral para determinar si un operador no fue eficiente recibiendo llamadas.

ring_time_threshold = (reciever_operators['avg_ring_time'].mean()).round()

ring_time_threshold
Out[51]:
64.0

Conclusión

De acuerdo a los cálculos, un operador no debe promediar más de 64 segundos para atender una llamada en una semana de labores para no ser considerado como "no eficiente".

Cantidad de llamadas realizadas

Observaremos como se distribuyen las proporciones de las cantidades de llamadas realizadas para los operadores que se desempeñan llamando clientes.

In [52]:
# Creando tabla de agregaciones sin los operadores "desconocidos" y para operadores que trabajan llamando a clientes

outgoing_operators = (calls_by_operators
                      .loc[(calls_by_operators['operator_id'] != "unknown") &
                          (calls_by_operators['reciever_operator'] == False)]
                      .reset_index(drop=True)
                     )

outgoing_operators
Out[52]:
call_week operator_id calls_count missed_calls_count missed_calls_proportion avg_call_duration median_call_duration external_calls internal_calls external_proportion avg_ring_time reciever_operator
0 35 880240.0 26 11 0.297297 1706.00 1706.0 26.0 0.0 1.0 NaN False
1 35 886146.0 10 0 0.000000 731.00 731.0 10.0 0.0 1.0 NaN False
2 35 887992.0 5 2 0.285714 142.00 142.0 5.0 0.0 1.0 NaN False
3 35 890416.0 36 27 0.428571 2111.50 1998.5 36.0 0.0 1.0 NaN False
4 35 890420.0 44 58 0.568627 2003.75 1850.5 44.0 0.0 1.0 NaN False
... ... ... ... ... ... ... ... ... ... ... ... ...
1198 48 970484.0 2 5 0.714286 75.00 75.0 2.0 0.0 1.0 NaN False
1199 48 970486.0 4 2 0.333333 150.00 150.0 4.0 0.0 1.0 NaN False
1200 48 972408.0 4 2 0.333333 200.00 200.0 4.0 0.0 1.0 NaN False
1201 48 972410.0 40 37 0.480519 1888.50 1888.5 40.0 0.0 1.0 NaN False
1202 48 973120.0 1 2 0.666667 5.00 5.0 1.0 0.0 1.0 NaN False

1203 rows × 12 columns

In [53]:
# Trazando histograma para variable ring_time

px.box(outgoing_operators,x='call_week' ,y='calls_count',
       title='Outgoing calls count by operators throughout weeks')

Conclusión intermedia

Las distribuciones de llamadas realizadas (exitosas) por semana nos demuestran que a partir de la tercera semana observada se reflejan valores atípicamente altos, sin embargo, para esta métrica estaremos considerando como no eficientes los operadores que no sobrepasen un de terminado umbral.

Definiremos dicho umbral basándonos en el promedio de esta distribución.

In [54]:
# Calculando umbral para determinar si un operador no fue eficiente recibiendo llamadas.

calls_count_threshold = (outgoing_operators['calls_count'].mean()).round()

calls_count_threshold
Out[54]:
24.0

Conclusión

De acuerdo a los cálculos, podemos concluir que los operadores no deben tener menos de 24 llamadas exitosas por semana para no ser considerados como no eficientes.

Comparando operadores eficientes vs no eficientes¶

Enriqueceremos nuestras tablas de operadores para categorizar si un operador resulta eficiente o no en determinada métrica y observaremos las distribuciones considerando esta categoría.

Regresar

Proporción de llamadas perdidas

In [55]:
# Incluyendo columna de eficiencia para métrica de llamadas perdidas

reciever_operators['missed_calls_efficiency'] = reciever_operators['missed_calls_proportion'] < missed_calls_threshold

reciever_operators[['calls_count','missed_calls_proportion', 'missed_calls_efficiency']]
Out[55]:
calls_count missed_calls_proportion missed_calls_efficiency
0 32 0.746032 False
1 8 0.804878 False
2 4 0.428571 False
3 113 0.315152 False
4 86 0.494118 False
... ... ... ...
4330 50 0.000000 True
4331 6 0.000000 True
4332 36 0.409836 False
4333 23 0.549020 False
4334 2 0.000000 True

4335 rows × 3 columns

In [56]:
# Creando agrupación para gráfico

missed_calls_group = (reciever_operators
                      .groupby(['call_week','missed_calls_efficiency'], as_index=False)
                      .agg({'calls_count':'sum',
                           'avg_call_duration':'mean',
                           'missed_calls_proportion':'mean'})
                     )

missed_calls_group.head()
Out[56]:
call_week missed_calls_efficiency calls_count avg_call_duration missed_calls_proportion
0 35 False 1875 662.124802 0.504622
1 35 True 569 236.002857 0.016718
2 36 False 2347 586.935743 0.442635
3 36 True 1352 321.036174 0.031889
4 37 False 3310 688.899624 0.468593
In [57]:
# Trazando gráfico de línea donde se refleje el comportamiento por semana

missed_calls_list = ['missed_calls_proportion','calls_count', 'avg_call_duration']

for i in missed_calls_list:
    
    call_lineplot = px.line(missed_calls_group, x='call_week', y=i,
                            color='missed_calls_efficiency',
                            title=f'Distribution of {i} throughout observed weeks'
                           )
    call_lineplot.show()

Conclusión

Los gráficos demuestran que existe una diferencia amplia en la cantidad de llamadas recibidas entre el grupo "eficiente" en el indicador de proporción de llamadas perdidas. Este mismo comportamiento se repite en la duración promedio de las llamadas. Una pequeña excepción parece ser en la semana 41 donde la cantidad de llamadas parece similar para ambos grupos.

A través de estos resultados pudieramos inferir que una de las razones o la razón por la que un grupo tiene un mejor tiempo de respuesta ante las llamadas es porque reciben una menor cantidad y porque tardan menos con los clientes en las mismas.

Comentario del revisor ¡Muy bien! 👏👏 El desarrollo de los gráficos, y la identificación de los grupos ineficientes.

Tiempo promedio para contestar una llamada

In [58]:
# Incluyendo columna de eficiencia para métrica de tiempo promedio para contestar una llamada

reciever_operators['avg_ring_time_efficiency'] = (reciever_operators['avg_ring_time'] <
                                                  ring_time_threshold)

reciever_operators[['avg_ring_time','avg_ring_time_efficiency']]
Out[58]:
avg_ring_time avg_ring_time_efficiency
0 70.25 False
1 25.00 True
2 NaN False
3 4.00 True
4 18.50 True
... ... ...
4330 286.00 False
4331 42.00 True
4332 25.00 True
4333 4.00 True
4334 88.00 False

4335 rows × 2 columns

In [59]:
# Creando agrupación para gráfico

ring_time_group = (reciever_operators
                   .groupby(['call_week','avg_ring_time_efficiency'], as_index=False)
                   .agg({'calls_count':'sum',
                         'avg_call_duration':'mean',
                        'avg_ring_time':'mean'})
                  )

ring_time_group.head()
Out[59]:
call_week avg_ring_time_efficiency calls_count avg_call_duration avg_ring_time
0 35 False 945 655.673016 111.144737
1 35 True 1499 370.095049 22.689670
2 36 False 1712 740.339237 102.261447
3 36 True 1987 336.208007 25.591117
4 37 False 1930 798.697131 119.098095
In [60]:
# Trazando gráfico de línea donde se refleje el comportamiento por semana

ring_time_list = ['avg_ring_time','calls_count', 'avg_call_duration']

for i in ring_time_list:
    
    call_lineplot = px.line(ring_time_group, x='call_week', y=i,
                            color='avg_ring_time_efficiency',
                            title=f'Distribution of {i} throughout observed weeks'
                           )
    call_lineplot.show()

Conclusión

Inicialmente, el grupo que no fue eficiente al momento de tomar la llamada tenía menos llamadas que el grupo con eficiencia en este mismo campo, de hecho, la diferencia entre ambos grupos para las primeras cuatro semanas fue de unos 80 segundos en promedio. Luego cuando ambos grupos comienzan a recibir más llamadas, la distancia entre la duración promedio se separa por mucho más (hasta unos 160 segundos).

Importante destacar, que el comportamiento del grupo que fue eficiente, se mantuvo estable en el transcurso de las semanas, tomandose no más de 30 segundos en tomar la llamada.

Por último, podemos observar que las duraciones de las llamadas para el grupo que no fue eficiente es considerablemente mayor que su contraparte, por lo tanto, podríamos inferir que la duración de la llamada es una de las principales causantes de que los operadores tarden en tomar una llamada a tiempo.

Cantidad de llamadas salientes

In [61]:
# Incluyendo columna de eficiencia para métrica de cantidad de llamadas exitosas realizadas

outgoing_operators['calls_count_efficiency'] = (outgoing_operators['calls_count'] >=
                                                calls_count_threshold)

outgoing_operators[['calls_count','calls_count_efficiency']]
Out[61]:
calls_count calls_count_efficiency
0 26 True
1 10 False
2 5 False
3 36 True
4 44 True
... ... ...
1198 2 False
1199 4 False
1200 4 False
1201 40 True
1202 1 False

1203 rows × 2 columns

In [62]:
# Creando agrupación para gráfico

outgoing_calls_group = (outgoing_operators
                        .groupby(['call_week','calls_count_efficiency'], as_index=False)
                        .agg({'calls_count':['sum','mean'],
                              'missed_calls_count':'sum',
                              'avg_call_duration':'mean'})
                       )

outgoing_calls_group.columns = ['call_week', 'calls_count_efficiency', 'calls_sum', 'calls_mean',
                                'missed_calls_count', 'avg_call_duration']

outgoing_calls_group.head()
Out[62]:
call_week calls_count_efficiency calls_sum calls_mean missed_calls_count avg_call_duration
0 35 False 259 10.791667 286 1033.515942
1 35 True 175 35.000000 191 1727.200000
2 36 False 123 5.347826 460 844.744444
3 36 True 266 33.250000 390 2153.270833
4 37 False 257 6.763158 531 846.149524
In [63]:
# Trazando gráfico de línea donde se refleje el comportamiento por semana

outgoing_calls_list = ['calls_mean', 'calls_sum','missed_calls_count', 'avg_call_duration']

for i in outgoing_calls_list:
    
    call_lineplot = px.line(outgoing_calls_group, x='call_week', y=i,
                            color='calls_count_efficiency',
                            title=f'Distribution of {i} throughout observed weeks'
                           )
    call_lineplot.show()

Conclusión

Desde el comienzo de las observaciones semanales, los operadores de ambos grupos presentaron un número colectivo de llamadas (por grupo) similar, sin embargo, el grupo de operadores que cumplieron con la cuota fue separando su cantidad total de llamadas como grupo al pasar de las semanas. En promedio, cada operador perteneciente al grupo categorizado como "eficiente" realizó unas 35 llamadas para la primera semana de observaciones, luego tuvo un pico de 80 llamadas por semana para luego estabilizarse en 72 llamadas exitosas por semana.

El grupo que quedó debajo del umbral establecido promedió no más de 10 llamadas por semana durante el período de observación.

Es importante destacar que ambos grupos de operadores tuvieron un comportamiento similar de acuerdo al conteo de llamadas no exitosas, sin embargo, el aumento considerable para las últimas 3 semanas de observaciones demuestra un nivel de compromiso superior a su contraparte para llegar a la cuota de llamadas exitosas que fue reflejando durante su ciclo de vida.

Por otra parte, el grupo que fue eficiente en la cantidad de llamadas realizadas, promedió un tiempo superior en cada llamada que su contraparte. Esto contrasta con la teoría que nos indicaba que los operadores realizaban menos llamadas dependiendo de la duración que hayan tenido las mismas.

Prueba de hipótesis¶

Comprobaremos diferentes hipótesis sobre los grupos de operadores considerando si están dentro o fuera del umbral de eficiencia establecido.

Regresar

Cantidad de llamadas entre operadores receptores¶

Comprobaremos la hipótesis sobre si los operadores receptores recibieron la misma cantidad de llamadas, donde:

$H0=$ La cantidad de llamadas es igual para ambos grupos

$H1=$ La cantidad de llamadas no es igual para ambos grupos

Regresar

In [64]:
# Creando filtros de operadores eficientes e ineficientes

calls_efficient_filter = (reciever_operators
                          .loc[reciever_operators['missed_calls_efficiency'] == True,'operator_id']
                          .reset_index(drop=True)
                         )

calls_inefficient_filter = (reciever_operators
                            .loc[reciever_operators['missed_calls_efficiency'] == False, 'operator_id']
                            .reset_index(drop=True)
                           )
In [65]:
# Creando tablas de llamadas filtradas por grupo de operadores en los grupos de la métrica

calls_efficient_operators = (filtered_calls
                            .loc[filtered_calls['operator_id'].isin(calls_efficient_filter)]
                            .reset_index(drop=True)
                            )

calls_inefficient_operators = (filtered_calls
                               .loc[filtered_calls['operator_id'].isin(calls_inefficient_filter)]
                               .reset_index(drop=True)
                              ) 

Comprobación de varianzas

In [66]:
# Estableciendo factor de significancia "alpha" para prueba de hipótesis

alpha = 0.05

# Realizando prueba de varianzas "levene"

calls_levene_st, calls_levene_pvalue = st.levene(calls_efficient_operators['calls_count'],
                                                 calls_inefficient_operators['calls_count'])

print(calls_levene_pvalue)
if calls_levene_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las varianzas no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las varianzas son iguales')
8.867754524586642e-58
Hipótesis nula rechazada, las varianzas no son iguales

Conclusión Intermedia

La prueba Levene nos deja saber que las varianzas entre las distribuciones de la cantidad de llamadas entre los 2 grupos de operadores no es igual.

Comprobación de medias

In [67]:
# Realizando prueba de medias ttest

calls_ttest_st, calls_ttest_pvalue = st.ttest_ind(calls_efficient_operators['calls_count'],
                                                  calls_inefficient_operators['calls_count'], equal_var=False)

print(calls_ttest_pvalue)
if calls_ttest_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las medias no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las medias son iguales')
1.1010150131122e-69
Hipótesis nula rechazada, las medias no son iguales

Conclusión

Los resultados de la prueba de medias nos refleja que las distribuciones de las cantidades de llamadas por grupo de operador no son iguales. Basándonos en los gráficos mostrados en la sección anterior, se evidencia que los operadores con eficiencia en la métrica de proporción de llamadas perdidas tuvo una menor cantidad de llamadas.

Duración de llamadas entre operadores receptores¶

Comprobaremos la hipótesis sobre si los operadores receptores duraron la misma cantidad de tiempo en sus llamadas considerando su eficiencia en la métrica de "eficiencia en tiempo de respuesta", donde:

$H0=$ La duración de las llamadas fue igual para ambos grupos

$H1=$ La duración de las llamadas no fue igual para ambos grupos

Regresar

In [68]:
# Creando filtros de operadores eficientes e ineficientes

ring_time_efficient_filter = (reciever_operators
                              .loc[reciever_operators['avg_ring_time_efficiency'] == True,'operator_id']
                              .reset_index(drop=True)
                             )

ring_time_inefficient_filter = (reciever_operators
                                .loc[reciever_operators['avg_ring_time_efficiency'] == False, 'operator_id']
                                .reset_index(drop=True)
                               )
In [69]:
# Creando tablas filtradas por grupo de operadores

ring_time_efficient_operators = (filtered_calls
                                 .loc[filtered_calls['operator_id'].isin(ring_time_efficient_filter)]
                                 .reset_index(drop=True)
                                )

ring_time_inefficient_operators = (filtered_calls
                                   .loc[filtered_calls['operator_id'].isin(ring_time_inefficient_filter)]
                                   .reset_index(drop=True)
                                  )

Comprobación de varianzas

In [70]:
# Estableciendo factor de significancia "alpha" para prueba de hipótesis

alpha = 0.05

# Realizando prueba de varianzas "levene"

ring_time_levene_st, ring_time_levene_pvalue = st.levene(ring_time_efficient_operators['call_duration'],
                                                         ring_time_inefficient_operators['call_duration'])

print(ring_time_levene_pvalue)
if ring_time_levene_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las varianzas no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las varianzas son iguales')
1.651311638674036e-27
Hipótesis nula rechazada, las varianzas no son iguales
Comentario del revisor Excelente, se probó igualdas de varianza antes de aplicar las pruebas t

Conclusión Intermedia

La prueba Levene nos deja saber que las varianzas entre las distribuciones del tiempo de duración de las llamadas entre los 2 grupos de operadores no es igual.

Comprobación de medias

In [71]:
# Realizando prueba de medias ttest

ring_time_ttest_st, ring_time_ttest_pvalue = st.ttest_ind(ring_time_efficient_operators['call_duration'],
                                                          ring_time_inefficient_operators['call_duration'])

print(ring_time_ttest_pvalue)
if ring_time_ttest_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las medias no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las medias son iguales')
7.291685322432611e-25
Hipótesis nula rechazada, las medias no son iguales

Conclusión

Los resultados de la prueba de medias nos refleja que las distribuciones de los tiempos de duración de las llamadas por grupo de operador no son iguales. Basándonos en los gráficos mostrados en la sección anterior, se evidencia que los operadores con eficiencia en la métrica de eficiencia en tiempos de atención de una llamada tuvo un mayor tiempo de duración entre las llamadas atendidas.

Duración de llamadas entre operadores emisores¶

Comprobaremos la hipótesis sobre si los operadores que debían hacer llamadas tardaron el mismo tiempo en las llamadas entre los grupos "eficientes" y "no eficientes", donde:

$H0=$ La duración de las llamadas es igual para ambos grupos

$H1=$ La duración de las llamadas no es igual para ambos grupos

Regresar

Comentario del revisor Correcto, siempre es importante ser muy claros con el significado de las hipotésis 💪💻
In [72]:
# Creando filtros de operadores eficientes e ineficientes

made_calls_efficient_filter = (outgoing_operators
                              .loc[outgoing_operators['calls_count_efficiency'] == True,'operator_id']
                              .reset_index(drop=True)
                             )

made_calls_inefficient_filter = (outgoing_operators
                                 .loc[outgoing_operators['calls_count_efficiency'] == False,'operator_id']
                                 .reset_index(drop=True)
                                )
In [73]:
# Creando tablas filtradas por grupo de operadores

made_calls_efficient_operators = (filtered_calls
                                  .loc[filtered_calls['operator_id'].isin(made_calls_efficient_filter)]
                                  .reset_index(drop=True))

made_calls_inefficient_operators = (filtered_calls
                                    .loc[filtered_calls['operator_id'].isin(made_calls_inefficient_filter)]
                                    .reset_index(drop=True))

Comprobación de varianzas

In [74]:
# Estableciendo factor de significancia "alpha" para prueba de hipótesis

alpha = 0.05

# Realizando prueba de varianzas "levene"

made_calls_levene_st, made_calls_levene_pvalue = st.levene(made_calls_efficient_operators['call_duration'],
                                                           made_calls_inefficient_operators['call_duration'])

print(made_calls_levene_pvalue)
if made_calls_levene_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las varianzas no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las varianzas son iguales')
7.148803372721451e-12
Hipótesis nula rechazada, las varianzas no son iguales

Conclusión Intermedia

La prueba Levene nos deja saber que las varianzas entre las distribuciones de la cantidad de llamadas entre los 2 grupos de operadores no es igual.

Comprobación de medias

In [75]:
# Realizando prueba de medias ttest

made_calls_ttest_st, made_calls_ttest_pvalue = st.ttest_ind(made_calls_efficient_operators['call_duration'],
                                                            made_calls_inefficient_operators['call_duration'],
                                                            equal_var=False)

print(made_calls_ttest_pvalue)
if made_calls_ttest_pvalue < alpha:
    
    print('Hipótesis nula rechazada, las medias no son iguales')
    
else:
    print('Hipótesis nula no rechazada, las medias son iguales')
1.3695283159104214e-11
Hipótesis nula rechazada, las medias no son iguales

Conclusión

De acuerdo a los resultados de la prueba, rechazamos la hipótesis de que la media de duración de llamadas para los operadores eficientes en esta métrica sea igual que su contraparte. Basándonos en el gráfico en la sección anterior donde se compara los tiempos de llamada de ambos grupos, podemos observar que el grupo de operadores que cumplió con el indicador de eficiencia tuvo un mayor tiempo de duración de llamadas en promedio.

Conclusión General y Recomendaciones¶

Iniciamos importando tablas con la información de las llamadas recibidas e información de los clientes que hacían dichas llamadas. Luego de realizar la exploración inicial de los datos pudimos evidenciar la presencia de valores ausentes y duplicados. Una vez realizado la investigación pertinente con respecto a los datos ausentes, nos deshicimos de los mismos.

Incluimos las semanas de cada llamada en la tabla para realizar agregaciones del desempeño de cada operador. En estas agregaciones incluimos los promedios del tiempo de llamada, cantidad de llamadas realizadas, proporción de llamadas perdidas, tiempo de espera antes de tomar una llamada, entre otros.

Buscamos la presencia de valores atípicos en los datos, y filtramos los mismos para quedarnos con un 95% de los datos de la distribución y evitar un mayor sesgo en nuestros resultados.

Para definir los umbrales que nos indicarían si un operador es o no eficiente en una métrica en particular, observamos la distribución de "proporción de llamadas perdidas por operador", "duración de tiempo en espera antes de atender una llamada" y "cantidad de llamadas realizadas". Promediamos los valores observados en el transcurso de las 14 semanas que contenían los datos mencionados y establecimos los umbrales en particular.

Incluimos una columna en los datos de eficiencia dependiendo de la métrica y luego probamos tres diferentes hipótesis donde nos consultamos si la "cantidad de llamadas" entre operadores del grupo de eficiencia por "proporción de llamadas perdidas" era igual, de la misma forma, "duración de llamadas" entre los operadores del grupo de eficiencia de "duración de tiempo en espera antes de atender una llamada", y por último la "duración de las llamadas" para los grupos de operadores cuyas funciones incluían contactar clientes.

Recomendaciones

En el caso de las métricas que afectan a los operadores que reciben llamadas, podemos observar que el cumplimiento de estas está directamente afectado por la cantidad de llamadas que reciben y la duración de las mismas. Recomendariamos un entrenamiento en solución eficiente de problemas para reducir el tiempo de las llamadas que reciben, así como también evaluar la posibilidad de incluir más operadores que puedan atender este tipo de casos.

Para los operadores que deben hacer llamadas, pudimos evidenciar que la cantidad de llamadas exitosas que realizan no están influenciadas por el tiempo que tardan con los clientes, pudieramos recomendar evaluar de cerca a estos operadores ya que pudieran estar siendo afectados por un factor motivacional.

En el siguiente link se puede ver la presentación asociada a este proyecto.

Regresar

Comentario del revisor Feedback de la presentación Has Realizado una presentación buena, Tiene un estilo gráfico adecuado y es clara en genera. Sin embargo hay un par de puntos que peuden benficiarte para dejar más claro lo que está ocurriendo. - Es importante dejar clara la problemática del problema. - Siempre que incluyamos gráficas es aconsejable que ocupen la pantalla completa o el mayor especio en pantalla.
Comentario del revisor

¡Qué gran trabajo has hecho! 👍 Podemos aprobar el proyecto.
Felicidades por la calidad de tu análisis. Te animo a que sigas aprendiendo y desafiando tu potencial en los próximos sprints. Estoy seguro de que tus habilidades y conocimientos serán valiosos en el futuro y te permitirán abordar problemas cada vez más complejos con éxito.

In [ ]: